package com.talkyun.utils;

import com.talkyun.utils.helper.RegxHelper;

import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

import static com.talkyun.utils.helper.RegxHelper.getInclude;

public class Reflector {
    private static Map<String, Field> FID2FIELD = new ConcurrentHashMap<String, Field>();
    private static Map<String, String[]> CLZ2FIELD = new ConcurrentHashMap<String, String[]>();
    private static Map<String, Method> KEY2MTH = new ConcurrentHashMap<String, Method>();
    private static final Pattern CGLIB_PATTERN = Pattern.compile("\\$\\$Enhancer(.*)CGLIB\\$\\$");
    private static final boolean SPRING = exists("org.springframework.beans.factory.FactoryBean");
    private static final Map<String, Object> CLZ2OBJECT = new ConcurrentHashMap<>();

    public static boolean exists(String clz) {
        try {
            Class.forName(clz);
        } catch (ClassNotFoundException e) {
            return false;
        }
        return true;
    }

    public static Class<?> getInterface(Object obj) {
        if (obj == null) {
            throw new NullPointerException();
        }
        // ADD spring factory-bean support
        Class<?> clz = getSpringFactoryBeanType(obj);
        if (clz == null) {
            clz = obj.getClass();
        }

        // ENHANCE support stand cglib and spring-cglib
        if (isCglibProxyClass(clz.getSimpleName())) {
            clz = clz.getSuperclass();
        }
        Class<?>[] faces = clz.isInterface() ? (new Class[]{clz}) : clz.getInterfaces();

        if (faces.length == 0) {
            return null;
        } else if (faces.length == 1) {
            return faces[0];
        } else {
            for (Class<?> face : faces) {
                String name = face.getName();
                if (!name.startsWith("java.") && !name.startsWith("javax.")) {
                    return face;
                }
            }
            return faces[0];
        }
    }

    public static Class<?> getGenericClass(Class<?> clazz) {
        Type type = clazz.getGenericSuperclass();

        if (type instanceof ParameterizedType) {
            ParameterizedType pt = ((ParameterizedType) type);
            Class<?> clazz2 = (Class<?>) pt.getRawType();
            Type[] tts = pt.getActualTypeArguments();
            TypeVariable tt = (TypeVariable) tts[0];
            String name = tt.getName();
            GenericDeclaration gd = tt.getGenericDeclaration();
            Type wt = pt.getOwnerType();
            return pt.getActualTypeArguments()[0].getClass();
        }
        return null;
    }

    public static boolean hasSpring() {
        return SPRING;
    }

    public static Object createArray(Class<?> clz, int len) {
        return Array.newInstance(clz, len);
    }

    public static Object create(String clz) {
        try {
            return create(Class.forName(clz));
        } catch (Throwable t) {
            throw new RuntimeException("Can't new " + clz, t);
        }
    }

    public static <T> T create(Class<T> clz) {
        try {
            return clz.newInstance();
        } catch (Throwable t) {
            throw new RuntimeException("Can't new " + clz, t);
        }
    }

    @SuppressWarnings("unchecked")
    public static <T> T create(Class<T> clz, Object[] args) {
        T object = (T) CLZ2OBJECT.get(clz.getName());
        if (object == null) {
            try {
                object = doCreate(clz, args);
            } catch (Throwable t) {
                throw new RuntimeException("Can't new " + clz, t);
            }
            CLZ2OBJECT.put(clz.getName(), object);
        }
        return object;
    }

    @SuppressWarnings("unchecked")
    private static <T> T doCreate(Class<T> clz, Object[] args) throws Exception {
        Constructor<?> last = null;

        Constructor<?>[] cs = clz.getConstructors();
        for (Constructor<?> c : cs) {
            Class<?>[] types = c.getParameterTypes();
            if (types.length != args.length) {
                continue;
            }

            boolean match = true;
            for (int i = 0; i < args.length; i++) {
                if (types[i].isInstance(args[i])) {
                    continue;
                }

                if (!isSameClass(types[i], args[i].getClass())) {
                    match = false;
                    break;
                }
            }

            if (match) {
                last = c;
                break;
            }
        }

        if (last == null) {
            throw new RuntimeException("Constructor not found!" + Arrays.toString(args));
        }

        return (T) last.newInstance(args);
    }

    public static Field[] getAllField(Class<?> clazz) {
        List<Field> lst = new ArrayList<Field>();
        while (clazz != null) {
            Field[] fds2 = clazz.getDeclaredFields();
            for (int i = 0; i < fds2.length; i++) {
                int mod = fds2[i].getModifiers();
                if (Modifier.isFinal(mod)) {
                    continue;
                }
                lst.add(fds2[i]);
            }
            clazz = clazz.getSuperclass();
        }
        return lst.toArray(new Field[lst.size()]);
    }

    public static String[] getAllFieldName(Object obj) {
        return getAllFieldName(obj.getClass());
    }

    public static String[] getAllFieldName(Class<?> clazz) {
        // check cache first
        String clzName = clazz.getName();
        String[] fields = CLZ2FIELD.get(clzName);
        if (fields != null) {
            return fields;
        }
        // lookup to Object
        Map<String, String> map = new HashMap<String, String>();
        List<String> lst = new ArrayList<String>();
        String prefix = "";
        while (clazz != null) {
            Field[] fds2 = clazz.getDeclaredFields();
            for (int i = 0; i < fds2.length; i++) {
                int mod = fds2[i].getModifiers();
                if (Modifier.isFinal(mod)) {
                    continue;
                }
                String field = fds2[i].getName();
                if (map.containsKey(field)) {
                    field = prefix + field;
                }
                lst.add(field);
                map.put(field, null);
                fds2[i].setAccessible(true);
                FID2FIELD.put(clzName + "@" + field, fds2[i]);
            }
            prefix = "p." + prefix;
            clazz = clazz.getSuperclass();
        }
        // fill cache
        fields = lst.toArray(new String[lst.size()]);
        CLZ2FIELD.put(clzName, fields);
        return fields;
    }

    public static Class<?> getFieldType(Class<?> clazz, String property) {
        String id = getFieldId(clazz, property);
        Field field = FID2FIELD.get(id);
        if (field == null) {
            if (!CLZ2FIELD.containsKey(clazz.getName())) {
                getAllFieldName(clazz);
            }
            field = FID2FIELD.get(id);
        }
        if (field == null) {
            // throw new RuntimeException(id + " not exists!");
            return null;
        } else {
            return field.getType();
        }
    }

    public static Method[] getAllMethod(Class<?> clazz) {
        return clazz.getDeclaredMethods();
    }

    public static Method getMethod(Class<?> clazz, String method) {
        String key = clazz.getName() + ":" + method;
        Method mth = KEY2MTH.get(key);
        if (mth == null) {
            // lookup from clazz
            Method[] mths = clazz.getDeclaredMethods();
            for (int i = 0; i < mths.length; i++) {
                Method tmp = mths[i];
                if (tmp.getName().equals(method)) {
                    if (mth == null) {
                        mth = tmp;
                    } else {
                        throw new RuntimeException("More than one method! " + clazz + ":" + method);
                    }
                }
            }
            // null or put in cache
            if (mth == null) {
                throw new RuntimeException("Method not found! " + clazz + ":" + method);
            } else {
                KEY2MTH.put(key, mth);
            }
        }

        return mth;
    }

    public static Method getMethod(Class<?> clazz, String method, int argsNum) {
        String key = (clazz.getName() + ":" + method + ":" + argsNum).toLowerCase();

        Method ref = KEY2MTH.get(key);
        if (ref == null) {
            // from current clazz
            ref = doMatch(method, argsNum, clazz.getDeclaredMethods());
            // from all parent
            ref = ref == null ? doMatch(method, argsNum, clazz.getMethods()) : ref;

            // local cache
            if (ref != null) {
                ref.setAccessible(true);
                KEY2MTH.put(key, ref);
            }
        }

        return ref;
    }

    public static void setValue(Object object, String property, Object value) {
        if (object == null) {
            return; // null target, return first
        }
        Class<?> clz = object.getClass();
        try {
            String key = clz.getName() + "@" + property;
            Field field = FID2FIELD.get(key);
            if (field == null) {
                getAllFieldName(object);
            }
            field = FID2FIELD.get(key);
            if (field == null) {
                throw new RuntimeException("Can't find field " + property);
            }
            field.set(object, value);
        } catch (Throwable t) {
            throw new RuntimeException("Fail to get " + object + " -> " + property, t);
        }
    }

    public static Object getValue(Object obj, String property) {
        if (obj == null) {
            return null;
        }
        Class<?> clz = obj.getClass();
        try {
            String key = clz.getName() + "@" + property;
            Field field = FID2FIELD.get(key);
            if (field == null) {
                int pos = property.indexOf('.');
                if (pos > 0) {
                    String p1 = property.substring(0, pos);
                    String p2 = property.substring(pos + 1);
                    return getValue(getValue(obj, p1), p2);
                }
                field = getField(clz, property);
                field.setAccessible(true);
                FID2FIELD.put(key, field);
            }
            return field.get(obj);
        } catch (Throwable t) {
            throw new RuntimeException("Fail to get " + obj + " -> " + property, t);
        }
    }

    public static String getValueAsString(Object obj, String property) {
        Object val = getValue(obj, property);
        if (val == null) {
            return null;
        }
        return val instanceof String ? (String) val : val.toString();
    }

    public static int getValueAsInt(Object obj, String property) {
        String val = getValueAsString(obj, property);
        return val == null ? 0 : Integer.parseInt(val);
    }

    // path = a.b.c[0].d
    public static Object getValueByPath(Object obj, String path) {
        Object ctx = obj;
        Object val = null;

        String arrayRegx = "\\[\\d{1,}\\]";

        String[] properties = path.trim().split("\\.");
        for (String property : properties) {
            if (RegxHelper.isInclude(property, arrayRegx)) {
                // 1 -> array property: property[x], reg:\[\d{1,}\]
                String p = RegxHelper.getHeadOther(property, arrayRegx);
                int no = Integer.parseInt(getInclude(getInclude(property, arrayRegx), "\\d{1,}"));
                val = getValue(ctx, p);
                if (val != null) {
                    val = Array.get(val, no);
                }
            } else if (ctx instanceof Map) {
                // 2 -> val is map
                val = ((Map) ctx).get(property);
            } else {
                // 3 -> simple property
                val = getValue(ctx, property);
            }

            // last -> ready next loop
            if (val == null) {
                break;
            }
            ctx = val;
        }

        return val;
    }

    public static Object invoke(Object object, String method, Object... args) {
        Method mo = getMethod(object.getClass(), method, args == null ? 0 : args.length);
        try {
            return mo.invoke(object, args);
        } catch (Throwable t) {
            throw new RuntimeException(t);
        }
    }

    public static Object invoke(Class<?> clazz, String method, Object obj, Object... args) {
        Method mo = getMethod(clazz, method, args == null ? 0 : args.length);
        try {
            return mo.invoke(obj, args);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    // 调用类静态方法
    public static Object invoke(Class<?> clazz, String method, Object... args) {
        Method mo = getMethod(clazz, method, args == null ? 0 : args.length);
        try {
            return mo.invoke(null, args);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static Field getField(@SuppressWarnings("rawtypes") Class clz, String property) {
        while (clz != null) {
            try {
                return clz.getDeclaredField(property);
            } catch (NoSuchFieldException nfe) {
                clz = clz.getSuperclass();
            }
        }
        return getEnumField(property);
    }

    private static Field getEnumField(String property) {
        try {
            return Enum.class.getDeclaredField(property);
        } catch (Exception e) {
            throw new RuntimeException("Can't find filed " + property);
        }
    }

    private static String getFieldId(Class<?> clazz, String field) {
        return clazz.getName() + "@" + field;
    }

    private static Class<?> getSpringFactoryBeanType(Object bean) {
        if (SPRING) {
            if (bean instanceof org.springframework.beans.factory.FactoryBean) {
                return ((org.springframework.beans.factory.FactoryBean) bean).getObjectType();
            }
        }
        return null;
    }

    private static boolean isCglibProxyClass(String clazzName) {
        return CGLIB_PATTERN.matcher(clazzName).find();
    }

    private static Method doMatch(String methodName, int argsNum, Method[] list) {
        Method method = null;
        for (Method tmp : list) {
            if (tmp.getName().equalsIgnoreCase(methodName)) {
                if (argsNum == tmp.getParameterTypes().length) {
                    if (method == null) {
                        method = tmp;
                    } else {
                        throw new RuntimeException("Can't handle override style! " + methodName + ":" + argsNum);
                    }
                }
            }
        }

        return method;
    }

    // 粗略判断相同类：
    private static boolean isSameClass(Class<?> c1, Class<?> c2) {
        if (c1 == c2) {
            return true;
        }

        String n1 = c1.getName();
        String n2 = c2.getName();
        if (n1.equals(n2)) {
            return true;
        }

        // int:java.lang.Integer, long:java.lang.Long
        if (c1.isPrimitive() && n2.startsWith("java.lang.")) {
            return true;
        }

        if (c2.isPrimitive() && n1.startsWith("java.lang.")) {
            return true;
        }

        return false;
    }
}